/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.wps.remote;

import java.awt.RenderingHints.Key;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.platform.GeoServerExtensions;
import org.geotools.data.Parameter;
import org.geotools.process.Process;
import org.geotools.process.ProcessFactory;
import org.geotools.text.Text;
import org.geotools.util.SimpleInternationalString;
import org.geotools.util.logging.Logging;
import org.opengis.feature.type.Name;
import org.opengis.util.InternationalString;

/**
 * A process factory that wraps a {@link RemoteProcessClient} and can be used to get information
 * about it and create the corresponding process.
 *
 * @author Alessio Fabiani, GeoSolutions
 */
public class RemoteProcessFactory implements ProcessFactory, RemoteProcessFactoryListener {

    public static final String WPS_VERSION = "1.0.0";

    /** The LOGGER */
    public static final Logger LOGGER =
            Logging.getLogger(RemoteProcessFactory.class.getPackage().getName());

    /**
     * Associates the generic sets of inputs and outputs declared by the remote service to the
     * {@link RemoteProcess} instance
     */
    private Map<Name, RemoteServiceDescriptor> descriptors =
            new ConcurrentHashMap<Name, RemoteServiceDescriptor>();

    /**
     * The list of currently running {@link RemoteProcess} instances; each of them is identified by
     * a unique pId generated by the {@link RemoteProcessClient} which must be used to honor the
     * asynchronous communication handshake. Remember that only the {@link RemoteProcessClient}
     * specific implementation knows how to handle the communication with the remote service.
     */
    private Map<Name, RemoteProcess> remoteInstances = new ConcurrentHashMap<Name, RemoteProcess>();

    /** The {@link RemoteProcessClient} instance */
    private RemoteProcessClient remoteClient;

    /**
     * Constructs a {@link RemoteProcessFactory} able to dynamically create stubs for the remote
     * communication
     */
    public RemoteProcessFactory() {
        try {
            List<RemoteProcessClient> availableRemoteClientInstances =
                    GeoServerExtensions.extensions(RemoteProcessClient.class);
            for (RemoteProcessClient ext : availableRemoteClientInstances) {
                if (ext.isEnabled()) {
                    remoteClient = ext;
                    remoteClient.init();
                    if (remoteClient.isEnabled()) {
                        remoteClient.registerProcessFactoryListener(this);
                        break;
                    }
                }
            }
        } catch (Exception e) {
            LOGGER.log(Level.SEVERE, e.getMessage(), e);
        }
    }

    /** @return the remoteClient */
    public RemoteProcessClient getRemoteClient() {
        return remoteClient;
    }

    /** @param remoteClient the remoteClient to set */
    public void setRemoteClient(RemoteProcessClient remoteClient) {
        this.remoteClient = remoteClient;
    }

    /** The Title of the {@link RemoteProcessFactory} */
    @Override
    public InternationalString getTitle() {
        return new SimpleInternationalString("Remote");
    }

    /** The currently available {@link RemoteProcess} stubs on the {@link RemoteProcessFactory} */
    @Override
    public Set<Name> getNames() {
        return descriptors.keySet();
    }

    /** Utility method to check if a {@link RemoteProcess} stub has been already registered */
    boolean checkName(Name name) {
        if (name == null) throw new NullPointerException("Process name cannot be null");
        if (!descriptors.containsKey(name)) {
            LOGGER.warning("Unknown process '" + name + "'");
            return false;
        }
        for (Name registeredService : descriptors.keySet()) {
            if (registeredService.getLocalPart().equalsIgnoreCase((name.getLocalPart()))) {
                return true;
            }
        }

        return true;
    }

    /** Creates a new {@link RemoteProcess} stub */
    @Override
    public Process create(Name name) throws IllegalArgumentException {
        synchronized (remoteInstances) {
            if (checkName(name)) {
                try {
                    RemoteProcess process =
                            new RemoteProcess(
                                    name, remoteClient, descriptors.get(name).getMetadata());
                    remoteInstances.put(name, process);
                    return process;
                } catch (Exception e) {
                    throw new RuntimeException(
                            "Error occurred cloning the prototype "
                                    + "algorithm... this should not happen",
                            e);
                }
            }
            return null;
        }
    }

    /** Get the {@link RemoteProcess} textual description */
    @Override
    public InternationalString getDescription(Name name) {
        synchronized (descriptors) {
            if (checkName(name)) return Text.text(descriptors.get(name).getDescription());
            return null;
        }
    }

    /** Get the {@link RemoteProcess} title */
    @Override
    public InternationalString getTitle(Name name) {
        synchronized (descriptors) {
            if (checkName(name)) return Text.text(descriptors.get(name).getTitle());
            return null;
        }
    }

    /** Get the {@link RemoteProcess} short name */
    public String getName(Name name) {
        if (checkName(name)) {
            return name.getLocalPart();
        }
        return null;
    }

    /** */
    @Override
    public boolean supportsProgress(Name name) {
        return true;
    }

    /** */
    @Override
    public String getVersion(Name name) {
        return WPS_VERSION;
    }

    /** Get the {@link RemoteProcess} textual description */
    @Override
    public Map<String, Parameter<?>> getParameterInfo(Name name) {
        synchronized (descriptors) {
            if (checkName(name)) return descriptors.get(name).getParamInfo();
            return null;
        }
    }

    /** Get the {@link RemoteProcess} textual description */
    @Override
    public Map<String, Parameter<?>> getResultInfo(Name name, Map<String, Object> inputs)
            throws IllegalArgumentException {
        synchronized (descriptors) {
            if (checkName(name)) return descriptors.get(name).getOutputInfo();
            return null;
        }
    }

    @Override
    public String toString() {
        return "RemoteProcessFactory";
    }

    /** */
    @Override
    public boolean isAvailable() {
        return true;
    }

    /** */
    @Override
    public Map<Key, ?> getImplementationHints() {
        return Collections.emptyMap();
    }

    /** Registers a new remote service */
    @Override
    public void registerProcess(RemoteServiceDescriptor serviceDescriptor) {
        Name name = serviceDescriptor.getName();
        if (descriptors.containsKey(name)) {
            LOGGER.warning("Service " + name + " already registered!");
            return;
        }
        descriptors.put(name, serviceDescriptor);
        create(name);
        LOGGER.info("Registered Service [" + name + "]");
    }

    /** De-registers a remote service */
    @Override
    public void deregisterProcess(Name name) {
        if (checkName(name)) {
            descriptors.remove(name);
            remoteInstances.remove(name);
            LOGGER.info("Deregistered Service [" + name + "]");
        }
    }
}
